273 - Constructor
好的,同学你好!很高兴能成为你的C++老师。构造函数 (Constructor) 是C++中非常基础且重要的概念,理解它对于掌握面向对象编程至关重要。别担心,我会一步一步带你学习,确保你能够透彻理解。
1. 标题与概要
标题: C++ 构造函数详解:对象的诞生奥秘
概要:
本节课我们将深入学习C++中的构造函数 (Constructor)。构造函数是一种特殊的类 (Class) 方法 (method),它在创建类对象 (object) 时被编译器 (compiler) 自动调用。它的主要任务是初始化对象的数据成员,确保对象在创建之初就处于一个合理、有效的状态。构造函数没有返回类型 (return type),并且其名称必须与类名完全相同。
2. 详细解释
让我们一步一步来揭开构造函数的神秘面纱:
什么是构造函数 (Constructor)?
想象一下,当你定义一个类 (Class) 时,你实际上是在设计一个蓝图。比如,我们设计一个“圆柱体” (Cylinder) 的蓝图。当我们根据这个蓝图创建一个具体的圆柱体实例(也就是对象 (object))时,我们就需要一个初始化的过程——比如设置圆柱体的底面半径和高。这个初始化的过程,就是由构造函数来完成的。
所以,构造函数是一个特殊的成员函数,它在程序创建一个属于特定类的对象时自动被调用。
构造函数的特殊性:
- 名称与类名相同: 这是硬性规定。如果你的类名叫
Cylinder
,那么它的构造函数名也必须是Cylinder
。 - 没有返回类型 (Return Type): 这是构造函数最显著的特征之一。它不像其他函数那样需要声明
void
、int
或其他类型。它就是没有返回类型,连void
都不写。因为它本身的目的就是“构造”对象,而不是返回某个值。 - 自动调用: 你不需要显式地去调用构造函数。当你声明一个类的对象时,编译器会自动为你匹配并调用合适的构造函数。
- 名称与类名相同: 这是硬性规定。如果你的类名叫
构造函数的作用:
构造函数的核心作用是初始化对象的数据成员。当一个对象被创建时,其成员变量可能包含一些随机的、无意义的“垃圾值”。构造函数可以为这些成员变量赋予初始值,使对象从一开始就处于一个已定义、可用的状态。
如何声明和定义构造函数?
构造函数在类的定义内部声明和定义,通常放在 public 访问控制区域,这样类的外部代码才能创建该类的对象。
无参数构造函数 (Default Constructor - 默认构造函数的一种形式):
这种构造函数不接受任何参数。它通常用于将成员变量初始化为预设的默认值。
例如,对于 Cylinder 类:
C++
`class Cylinder { private: double base_radius; double height;
public: // 无参数构造函数 Cylinder() { base_radius = 1.0; // 默认半径 height = 1.0; // 默认高度 std::cout << “无参数构造函数被调用!” << std::endl; } // … 其他成员 … };`
当你这样创建对象时:
Cylinder c1;
,这个无参数构造函数就会被调用。带参数的构造函数 (Parameterized Constructor):
这种构造函数接受一个或多个参数,允许在创建对象时就传递初始值。
例如:
C++
`class Cylinder { private: double base_radius; double height;
public: // 无参数构造函数 (可以保留,也可以不保留,取决于你的设计) Cylinder() { base_radius = 1.0; height = 1.0; std::cout << “无参数构造函数被调用!” << std::endl; }
Code1
2
3
4
5
6
7// 带参数的构造函数
Cylinder(double r_param, double h_param) {
base_radius = r_param;
height = h_param;
std::cout << "带参数构造函数被调用!参数为 radius=" << base_radius << ", height=" << height << std::endl;
}
// ... 其他成员 ...};`
当你这样创建对象时:
Cylinder c2(2.5, 10.0);
,这个带参数的构造函数就会被调用,r_param
接收2.5
,h_param
接收10.0
。参数传递:
在上面的例子中,double r_param 和 double h_param 是通过值传递 (pass by value) 的。这意味着传递给构造函数的参数值会被复制一份到构造函数内部的 r_param 和 h_param。对于基本数据类型(如 double, int),这通常是没问题的。
构造函数重载 (Constructor Overloading):
一个类可以有多个构造函数,只要它们的参数列表(参数的个数、类型或顺序)不同即可。这就是函数重载在构造函数上的应用。这为对象的创建提供了灵活性。
比如,Cylinder 类可以同时拥有无参数构造函数和带两个 double 参数的构造函数,如上例所示。编译器会根据你创建对象时提供的参数来决定调用哪个构造函数。
C++
Cylinder cylinder1; // 调用 Cylinder() Cylinder cylinder2(5.0, 2.0); // 调用 Cylinder(double, double)
默认构造函数 (Default Constructor):
“默认构造函数”这个术语稍微有点复杂,它可以指两种情况:
- 用户定义的无参数构造函数: 就像我们上面写的
Cylinder() { ... }
。 编译器自动生成的构造函数:非常重要的一点: 如果你在类中 没有定义任何 构造函数(一个都没有写),那么C++编译器 (compiler) 会自动为你生成一个公开的 (public)、无参数的构造函数。这个自动生成的构造函数函数体是空的,它不会对成员变量做任何显式的初始化(除非成员变量有类内初始值)。 例如,如果我们有这样的类:
但是,如果你自己定义了任何一种构造函数(哪怕是一个带参数的构造函数),编译器就不会再自动生成那个无参数的默认构造函数了。 例如:
如果这种情况下你仍然需要一个无参数的构造函数,你就必须自己显式地定义它。
Code1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38**C++**
`class Box {
private:
double length; // 没有类内初始值
double width = 1.0; // 有类内初始值
double height;
public:
void display() {
std::cout << "Length: " << length << ", Width: " << width << ", Height: " << height << std::endl;
}
};
// 在 main 函数中
Box b1; // 合法!编译器提供了默认构造函数
// b1.length 和 b1.height 的值是未定义的 (垃圾值)
// b1.width 的值是 1.0 (因为有类内初始值)
b1.display();`
**C++**
`class Box {
private:
double length;
double width;
double height;
public:
// 只定义了一个带参数的构造函数
Box(double l, double w, double h) {
length = l;
width = w;
height = h;
}
};
// 在 main 函数中
// Box b1; // 错误!编译不通过!
// 因为我们定义了构造函数,编译器不再提供默认的无参数构造函数了。
// 必须这样创建:
Box b2(10, 5, 2); // 正确,调用我们定义的带参数构造函数`
- 用户定义的无参数构造函数: 就像我们上面写的
构造函数与访问权限 (Access Specifiers):
构造函数通常声明在 public 部分,这样类的外部代码才能创建对象。如果将构造函数声明在 private 或 protected 部分,那么就不能从类的外部直接创建对象了(这在某些设计模式如单例模式中会有特定用途,但初学阶段我们先关注 public)。
调试时观察构造函数:
使用调试器 (debugger) 是理解构造函数何时被调用的好方法。你可以在构造函数的开头设置一个断点 (breakpoint)。当程序运行到创建对象的语句时,调试器会停在构造函数的断点处,你就可以观察到构造函数被调用,以及它是如何初始化成员变量的。
为什么之前没有构造函数的代码也能工作?
就像上面提到的,如果你在之前的代码中没有为类编写任何构造函数,编译器会为你提供一个默认的、无参数的、空的构造函数。这个构造函数虽然什么也不做(不显式初始化成员),但它使得对象的创建语法(如 Cylinder c1;)是合法的。成员变量此时可能会有类内初始值(如果定义了),或者保持未初始化的状态(包含垃圾值)。
3. 代码示例
下面是一个完整的 Cylinder
类示例,演示了不同构造函数的用法:
C++
`#include
class Cylinder { private: double base_radius; // 成员变量 (member variable): 底面半径 double height; // 成员变量: 高度
public: // 构造函数 (Constructors) // 1. 无参数构造函数 Cylinder() { base_radius = 1.0; // 默认值 height = 1.0; // 默认值 std::cout << “Cylinder() 无参数构造函数被调用。 radius = “ << base_radius << “, height = “ << height << std::endl; }
1 | // 2. 带两个参数的构造函数 |
};
int main() { std::cout << “—- 创建 cylinder1 (使用无参数构造函数) —-“ << std::endl; Cylinder cylinder1; // 调用 Cylinder() std::cout << “cylinder1 体积: “ << cylinder1.volume() << std::endl; std::cout << std::endl;
1 | std::cout << "--- 创建 cylinder2 (使用带两个参数的构造函数) ---" << std::endl; |
}`
预期输出:
-- 创建 cylinder1 (使用无参数构造函数) --- Cylinder() 无参数构造函数被调用。 radius = 1, height = 1 cylinder1 体积: 3.14159 --- 创建 cylinder2 (使用带两个参数的构造函数) --- Cylinder(double, double) 带参数构造函数被调用。 radius = 2, height = 5 cylinder2 体积: 62.8319 --- 创建 cylinder3 (使用带一个参数的构造函数) --- Cylinder(double) 带一个参数的构造函数被调用。 radius = 3, height = 1 cylinder3 体积: 28.2743
4. QA 闪卡 (Flash Cards)
问: 什么是构造函数 (Constructor)?
答: 一种特殊的类成员函数,在创建类的对象时自动调用,用于初始化对象。
问: 构造函数可以有返回类型 (return type) 吗?
答: 不可以,连 void 都不行。
问: 构造函数的名称必须是什么?
答: 必须与类 (Class) 的名称完全相同。
问: 如果我没有为我的类编写任何构造函数,会发生什么?
答: 编译器 (compiler) 会自动生成一个公开的 (public)、无参数的、函数体为空的默认构造函数。
问: 如果我只编写了一个带参数的构造函数,我还能使用 MyClass obj; 这种形式创建对象吗?
答: 不能。一旦你定义了任何构造函数,编译器就不再自动生成默认的无参数构造函数。如果需要,你必须自己显式定义一个无参数构造函数。
问: 构造函数通常在类的哪个访问区域声明?为什么?
答: 通常在 public 区域声明,以便类的外部代码可以创建该类的对象。
5. 常见误解或易犯错误
给构造函数添加返回类型:
新手常犯的错误是尝试给构造函数声明一个返回类型,比如 void Cylinder() { … }。这是错误的,会导致编译错误,或者编译器可能不会将其识别为构造函数。
- 错误:
void MyClass() {}
- 正确:
MyClass() {}
- 错误:
构造函数名称与类名不完全匹配:
大小写、拼写错误都会导致编译器无法识别其为构造函数。
- 类名:
Cylinder
- 错误:
cylinder() {}
或Cylinder_Constructor() {}
- 正确:
Cylinder() {}
- 类名:
忘记初始化所有成员变量:
虽然构造函数会被调用,但如果你在构造函数体内忘记给某个成员变量赋值,那么该成员变量的值(如果它没有类内初始值)将是未定义的(垃圾值)。
对编译器生成的默认构造函数的误解:
- 误解1: 编译器总是会生成一个默认构造函数。 纠正: 只有当你 没有 提供任何自定义构造函数时,编译器才会生成。
- 误解2: 编译器生成的默认构造函数会把所有成员初始化为0或
nullptr
。 纠正: 编译器生成的默认构造函数是空的,它不对成员进行初始化,除非成员有类内初始值(例如int count = 0;
在类定义中)。否则,基本类型的成员变量将具有不确定的值。
构造函数私有化 (Private) 后的困惑:
将构造函数声明为 private 会阻止在类外部直接创建对象。例如 Cylinder c1; 会编译失败。这不是一个“错误”,而是一种设计选择(例如用于实现单例模式),但初学者可能会因此感到困惑。
在创建对象时,参数类型或数量与任何已定义的构造函数都不匹配:
例如,如果 Cylinder 只有 Cylinder() 和 Cylinder(double, double),那么 Cylinder c(10); 就会编译错误,因为没有只接受一个 int (或可转换为 double 的单个参数) 的构造函数(除非我们添加了 Cylinder(double))。
6. 编码练习
现在,轮到你来实践了!下面是一个 Book
类的骨架,请你为它填充构造函数。
C++
`#include
public: // TODO 1: 实现一个无参数构造函数 // 将 title 初始化为 “Unknown Title” // 将 author 初始化为 “Unknown Author” // 将 pages 初始化为 0 // 将 price 初始化为 0.0 // 在构造函数内部打印一条消息,例如 “Book default constructor called.” / 在这里填入你的代码 /
1 | // TODO 2: 实现一个带参数的构造函数 |
};
int main() { std::cout << “Creating book1 using default constructor:\n”; Book book1; book1.displayBookInfo();
1 | std::cout << "\\nCreating book2 using parameterized constructor:\\n"; |
}`
请你思考一下,如何在标记 /* 在这里填入你的代码 */
的地方完成这两个构造函数的定义。完成后,将你的代码发给我,我会帮你批改并给出反馈!加油!
1 |
|
太棒了!你的代码写得非常出色!看得出来你已经很好地理解了构造函数的概念和用法。
7. 对你的解答的评价与正确答案
评价: 你的解答是 100% 正确的!堪称完美!
- 无参数构造函数: 你准确地实现了无参数构造函数
Book()
,正确初始化了所有成员变量为指定的默认值,并且打印了提示信息。 - 带参数构造函数: 你也准确地实现了带四个参数的构造函数
Book(std::string title_param, std::string author_param, int pages_param, double price_param)
,使用传入的参数初始化了对象的成员变量,并且打印了相应的提示信息。 - 命名规范: 在带参数的构造函数中,你使用了
_param
后缀来区分参数名和成员变量名(例如title_param
和title
),这是一个很好的习惯,可以避免混淆。
你的代码完全符合练习的要求,并且清晰易读。
标准答案与解释:
你的代码就是标准答案!为了更完整,我这里再展示一下,并额外提一种C++中推荐的初始化方式:成员初始化列表 (Member Initializer List)。
C++
`#include
public: // 1. 无参数构造函数 (使用成员初始化列表) Book() : title(“Unknown Title”), author(“Unknown Author”), pages(0), price(0.0) { // title = “Unknown Title”; // 这些赋值现在在初始化列表中完成 // author = “Unknown Author”; // pages = 0; // price = 0.0; std::cout << “Book Default constructor called.” << std::endl; }
1 | // 2. 带参数的构造函数 (使用成员初始化列表) |
};
int main() { std::cout << “Creating book1 using default constructor:\n”; Book book1; book1.displayBookInfo();
1 | std::cout << "\\nCreating book2 using parameterized constructor:\\n"; |
}`
关于成员初始化列表 (Member Initializer List) 的说明:
在你写的代码中,你在构造函数的函数体内部使用了赋值语句来初始化成员变量,例如 title = "Unknown Title";
。这在功能上是完全正确的。
不过,C++ 还提供了另一种在构造函数中初始化成员变量的方式,叫做“成员初始化列表”。它写在构造函数参数列表之后,函数体 {}
之前,以冒号 :
开始,各个成员的初始化用逗号 ,
分隔。
例如:
Book() : title(“Unknown Title”), author(“Unknown Author”), pages(0), price(0.0) { / 函数体 / }
为什么推荐使用成员初始化列表?
- 效率: 对于类类型的成员变量(比如
std::string
)或者const
成员,使用初始化列表是直接进行“初始化”操作。而如果在构造函数体内赋值,则是先进行默认初始化,然后再进行一次“赋值”操作。对于复杂对象,直接初始化通常比“默认初始化+赋值”更高效。对于基本数据类型(如int
,double
),两者差别不大,但养成使用初始化列表的习惯是好的。 - 必要性:
const
成员:const
成员变量必须在初始化列表中进行初始化,不能在构造函数体内赋值(因为const
变量一旦创建其值就不能改变)。- 引用成员 (Reference members): 引用也必须在初始化列表中初始化。
- 没有默认构造函数的基类或成员类: 当继承或包含一个没有默认构造函数的类时,也必须在初始化列表中显式调用其构造函数。
虽然在当前这个 Book
类的例子中,你的赋值方式完全没有问题,但了解并适时使用成员初始化列表是一个C++程序员进阶的标志。不过别担心,这是我们后续可以慢慢学习和习惯的内容。你目前对构造函数的理解和应用已经非常棒了!
8. 建议与扩展问题供你研究
你已经掌握了构造函数的基本用法,非常好!接下来,你可以思考和探索以下问题,这将帮助你更深入地理解类和对象:
this
指针: 在成员函数(包括构造函数)内部,如何引用对象自身的成员?了解一下this
指针。例如,如果参数名和成员变量名相同,this->member_name = member_name;
。- 委托构造函数 (Delegating Constructors) (C++11): 一个构造函数能否调用同一个类中的另一个构造函数来完成部分初始化工作?
- 拷贝构造函数 (Copy Constructor): 当用一个已存在的对象去创建并初始化另一个新对象时(例如
Book book4 = book2;
或Book book5(book2);
),会调用哪种特殊的构造函数?它有什么特点和作用?(这是非常重要的一个概念) - 析构函数 (Destructor): 与构造函数相对,对象在销毁时会自动调用的特殊成员函数是什么?它有什么作用?(例如,释放构造函数中分配的资源)
- 类成员的初始化顺序: 成员变量在初始化列表中的初始化顺序是由它们在初始化列表中的书写顺序决定的,还是由它们在类中声明的顺序决定的?动手写个小程序验证一下。
- 如果
Book
类有一个const
成员变量,比如const std::string ISBN;
,你将如何修改构造函数来初始化它?
这些问题将引导你学习更多关于C++类设计的知识。不要急于求成,一个一个地去理解和实践。
再次恭喜你完成了练习,并且做得非常好!继续保持这种学习的热情和探索精神,你一定能学好C++的!如果你有任何其他问题,随时问我。